/*
 * SubSonic - http://subsonicproject.com
 * 
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an 
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
*/

using System;
using System.ComponentModel;
using System.Data;
using System.Runtime.Serialization;
using System.Text;
using System.Web.UI.WebControls;
using System.Xml.Serialization;
using SubSonic.Utilities;
using System.Collections.Generic;

namespace SubSonic
{
    /// <summary>
    /// Base class for persisting objects. Follows the "Active Record Design Pattern".
    /// You can read more on this pattern at http://en.wikipedia.org/wiki/Active_Record
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [Serializable]
    public abstract class AbstractRecord<T> : ISerializable
        where T : AbstractRecord<T>, new()
    {

        #region "Constructors"

        public AbstractRecord()
            : base()
        {
        }

        #endregion

        #region State Properties

        private string _auditMessage;

        private TableSchema.TableColumnCollection _dirtyColumns = new TableSchema.TableColumnCollection();

        /// <summary>
        /// A collection of column's who's values have been changed during instance scope. Used
        /// for Update commands.
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public TableSchema.TableColumnCollection DirtyColumns
        {
            get { return _dirtyColumns; }
            set { _dirtyColumns = value; }
        }
        
        private bool _isLoaded = false;
        internal bool _isNew = true;

        /// <summary>
        /// Gets or sets a value indicating whether this instance is loaded.
        /// </summary>
        /// <value><c>true</c> if this instance is loaded; otherwise, <c>false</c>.</value>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsLoaded
        {
            get { return _isLoaded; }
            set { _isLoaded = value; }
        }

        /// <summary>
        /// Whether or not this is a new record. The value is <c>true</c> if this object
        /// has yet to be persisted to a database, otherwise it's <c>false</c>.
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsNew
        {
            get { return _isNew; }
            set { _isNew = value; }
        }

        /// <summary>
        /// Whether or not the value of this object differs from that of the persisted
        /// database. This value is <c>true</c>if data has been changed and differs from DB,
        /// otherwise it is <c>false</c>.
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public bool IsDirty
        {
            get { return columnSettings.IsDirty; }
        }

        /// <summary>
        /// Name of the table in the database that contains persisted data for this type
        /// </summary>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string TableName
        {
            get { return BaseSchema.TableName; }
        }

        /// <summary>
        /// Empty virtual method that may be overridden in user code to perform operations
        /// at the conclusion of SetDefaults() invocation
        /// </summary>
        public virtual void Initialize() {}

        /// <summary>
        /// Empty virtual method that may be overridden in user code to perform operations
        /// at the conclusion of Load() invocation
        /// </summary>
        protected virtual void Loaded() {}

        /// <summary>
        /// Automatically called upon object creation. Sets IsNew to <c>true</c>;
        /// </summary>
        public void MarkNew()
        {
            _isNew = true;
        }

        /// <summary>
        /// Called after any property is set. Sets IsDirty to <c>true</c>.
        /// </summary>
        public void MarkClean()
        {
            columnSettings.IsDirty = false;
        }

        /// <summary>
        /// Called after Update() invokation. Sets IsNew to <c>false</c>.
        /// </summary>
        public void MarkOld()
        {
            _isNew = false;
        }

        #endregion


        #region Object Overrides

        /// <summary>
        /// Creates a HTML formatted text represententation of the contents of the current record
        /// </summary>
        /// <returns></returns>
        public string Inspect()
        {
            return Inspect(true);
        }

        /// <summary>
        /// Creates a formatted text represententation of the contents of the current record
        /// </summary>
        /// <param name="useHtml">Whether or not the results should be formatted using HTML</param>
        /// <returns></returns>
        public string Inspect(bool useHtml)
        {
            StringBuilder sb = new StringBuilder();
            string sOut;
            if(useHtml)
            {
                sb.Append("<table><tr><td colspan=2><h3>" + BaseSchema.Name + " Inspection</h3></td></tr>");

                foreach(TableSchema.TableColumn col in BaseSchema.Columns)
                    sb.Append("<tr><td><span style=\"font-weight:bold\">" + col.ColumnName + "</span></td><td>" + GetColumnValue<object>(col.ColumnName) + "</td></tr>");
                sb.Append("</table>");
                sOut = sb.ToString();
            }
            else
            {
                sb.AppendLine("#################" + BaseSchema.Name + " Inspection ####################");

                foreach(TableSchema.TableColumn col in BaseSchema.Columns)
                    sb.AppendLine(col.ColumnName + ": " + GetColumnValue<object>(col.ColumnName));
                sb.AppendLine("#############################################################################");
                sOut = sb.ToString();
            }
            return sOut;
        }

        /// <summary>
        /// Returns the simple string representation of the current record. By convention, this is the "descriptor" column (#2)
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            string result = String.Empty;
            if(columnSettings != null)
            {
                if(columnSettings.Count > 1)
                    result = columnSettings[1].CurrentValue.ToString();
                else if(columnSettings.Count == 1)
                    result = columnSettings[0].CurrentValue.ToString();
            }
            return result;
        }

        #endregion


        #region DB Properties/Methods

        protected static TableSchema.Table table;
        private string _providerName;

        /// <summary>
        /// The column settings hold the current values of the object in a collection
        /// so that reflection is not needed in the base class to fill out the commands
        /// </summary>
        private TableSchema.TableColumnSettingCollection columnSettings = null;

        /// <summary>
        /// The base static class that holds all schema info for the table.
        /// The class must be instanced at least once to populate this table.
        /// </summary>
        protected static TableSchema.Table BaseSchema
        {
            get
            {
                if(table == null)
                    new T();
                return table;
            }
            set { table = value; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is schema initialized.
        /// The schema is considered initialized if the underlying TableSchema.Table
        /// object is not null, and column collection has been loaded with one or more columns
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is schema initialized; otherwise, <c>false</c>.
        /// </value>
        protected static bool IsSchemaInitialized
        {
            get { return (table != null && table.Columns != null && table.Columns.Count > 0); }
        }

        /// <summary>
        /// Gets or sets the name of the provider.
        /// </summary>
        /// <value>The name of the provider.</value>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string ProviderName
        {
            get { return _providerName; }
            protected set { _providerName = value; }
        }

        /// <summary>
        /// Returns a default setting per data type
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        protected static object GetDefaultSetting(TableSchema.TableColumn column)
        {
            return Utility.GetDefaultSetting(column);
        }

        /// <summary>
        /// Return the underlying DbType for a column
        /// </summary>
        /// <param name="columnName">The name of the column whose type is to be returned</param>
        /// <returns></returns>
        public DbType GetDBType(string columnName)
        {
            TableSchema.TableColumn col = BaseSchema.GetColumn(columnName);
            return col.DataType;
        }

        /// <summary>
        /// Sets the Primary Key of the object
        /// </summary>
        /// <param name="oValue"></param>
        protected void SetPrimaryKey(object oValue)
        {
            columnSettings.SetValue(BaseSchema.PrimaryKey.ColumnName, oValue);
        }

        /// <summary>
        /// Returns the current value of the primary key
        /// </summary>
        /// <returns></returns>
        public object GetPrimaryKeyValue()
        {
            //return columnSettings.GetValue(table.PrimaryKey.ColumnName).ToString();
            return columnSettings.GetValue<object>(BaseSchema.PrimaryKey.ColumnName);
        }
        
        /// <summary>
        /// Sets a value for a particular column in the record
        /// </summary>
        /// <param name="columnName">Name of the column, as defined in the database</param>
        /// <param name="oValue">The value to set the type to</param>
        public void SetColumnValue(string columnName, object oValue)
        {
            columnSettings = columnSettings ?? new TableSchema.TableColumnSettingCollection();
            //add the column to the DirtyColumns
            //if this instance has already been loaded
            //and this is a change to existing values
            if(IsLoaded && !IsNew)
            {
                TableSchema.Table schema = GetSchema();
                object oldValue = null;
                string oldValueMsg = "NULL";
                string newValueMsg = "NULL";
                bool areEqualOrBothNull = false;

                try
                {
                    oldValue = columnSettings.GetValue(columnName);
                }
                catch {}

                if(oldValue == null && oValue == null)
                    areEqualOrBothNull = true;
                else
                {
                    if(oldValue != null)
                    {
                        oldValueMsg = oldValue.ToString();
                        areEqualOrBothNull = oldValue.Equals(oValue);
                    }
                    if(oValue != null)
                        newValueMsg = oValue.ToString();
                }

                TableSchema.TableColumn dirtyCol = schema.GetColumn(columnName);

                if(dirtyCol != null && !areEqualOrBothNull)
                {
                    string auditMessage = "Value changed from " + oldValueMsg + " to " + newValueMsg + Environment.NewLine;

                    if(DirtyColumns.GetColumn(columnName) != null)
                    {
                        DirtyColumns.Remove(DirtyColumns.GetColumn(columnName));
                        //persist the audit
                        auditMessage = dirtyCol.AuditMessage + auditMessage;
                    }

                    dirtyCol.AuditMessage = auditMessage;
                    DirtyColumns.Add(dirtyCol);
                }
            }
            columnSettings.SetValue(columnName, oValue);            
        }

        internal void SetDefaultColumnValue(string columnName, object oValue)
        {
            columnSettings = columnSettings ?? new TableSchema.TableColumnSettingCollection();
            columnSettings.SetDefaultValue(columnName, oValue); 
        }

        internal void SetOriginalColumnValue(string columnName, object oValue)
        {
            columnSettings = columnSettings ?? new TableSchema.TableColumnSettingCollection();
            columnSettings.SetOriginalValue(columnName, oValue);
        }

        /// <summary>
        /// Returns the current value of a column.
        /// </summary>
        /// <param name="columnName">Name of the column, as defined in the database</param>
        /// <returns></returns>
        public CT GetColumnValue<CT>(string columnName)
        {
            CT oOut = default(CT);

            if(columnSettings != null)
                oOut = columnSettings.GetValue<CT>(columnName);

            return oOut;
        }
        
        public CT GetDefaultColumnValue<CT>(string columnName)
        {
            CT oOut = default(CT);

            if (columnSettings != null)
            {
                 if(columnSettings.Contains(columnName.ToLower()))
                    oOut = (CT)columnSettings[columnName.ToLower()].DefaultValue;
            }

            return oOut;
        }
        
        public CT GetOriginalColumnValue<CT>(string columnName)
        {
            CT oOut = default(CT);

            if (columnSettings != null)
            {
                if (columnSettings.Contains(columnName.ToLower()))
                    oOut = (CT)columnSettings[columnName.ToLower()].OriginalValue;
            }

            return oOut;
        }
        
        /// <summary>
        /// Returns the underly TableSchema.Table object for the given record
        /// </summary>
        /// <returns></returns>
        public TableSchema.Table GetSchema()
        {
            return BaseSchema;
        }

        #endregion


        #region Fetchers

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(int keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(int? keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(long keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(long? keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(decimal keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(decimal? keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(Guid keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(Guid? keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        public static T FetchByID(string keyValue)
        {
            return fetchByID(keyValue);
        }

        /// <summary>
        /// Returns a record for this keyValue
        /// </summary>
        /// <param name="keyValue">Key Value</param>
        /// <returns></returns>
        private static T fetchByID(object keyValue)
        {
            if(keyValue == null)
                return null;

            //makes sure the table schema is loaded
            T item = new T();

            //build the query
            Query q = new Query(BaseSchema).WHERE(BaseSchema.PrimaryKey.ColumnName, Comparison.Equals, keyValue);

            //load the reader
            using(IDataReader rdr = DataService.GetSingleRecordReader(q.BuildSelectCommand()))
            {
                if(rdr.Read())
                    item.Load(rdr);
                rdr.Close();
            }
            if(!item._isNew && item.IsLoaded)
                return item;
            return null;
        }

        /// <summary>
        /// Returns all records for this table
        /// </summary>
        /// <returns>IDataReader</returns>
        public static IDataReader FetchAll()
        {
            Query q = new Query(BaseSchema);
            CheckLogicalDelete(q);

            IDataReader rdr = DataService.GetReader(q.BuildSelectCommand());
            return rdr;
        }

        /// <summary>
        /// Returns all records for this table, ordered
        /// </summary>
        /// <returns>Generic Typed List</returns>
        /// <param name="orderBy">OrderBy object used to specify sort behavior</param>
        public static IDataReader FetchAll(OrderBy orderBy)
        {
            Query q = new Query(BaseSchema);
            q.OrderBy = orderBy;
            CheckLogicalDelete(q);

            return DataService.GetReader(q.BuildSelectCommand());
        }

        /// <summary>
        /// Returns all records for the given column/parameter, ordered by the passed in orderBy
        /// The expression for this is always "column=parameter"
        /// </summary>
        /// <param name="columnName">Name of the column to use in parmeter statement</param>
        /// <param name="oValue">Value of the column</param>
        /// <param name="oValue">Column to order by</param>
        /// <param name="orderBy">Ordering of results</param>
        /// <returns>IDataReader</returns>
        public static IDataReader FetchByParameter(string columnName, object oValue, OrderBy orderBy)
        {
            Query q = new Query(BaseSchema);
            q.OrderBy = orderBy;
            q.AddWhere(columnName, oValue);
            CheckLogicalDelete(q);

            return DataService.GetReader(q.BuildSelectCommand());
        }

        /// <summary>
        /// Returns all records for the given column/parameter
        /// The expression for this is always "column=parameter"
        /// </summary>
        /// <param name="columnName">The name of the column, as defined in the database</param>
        /// <param name="oValue">The value to match when fetching</param>
        /// <returns>IDataReader</returns>
        public static IDataReader FetchByParameter(string columnName, object oValue)
        {
            Query q = new Query(BaseSchema);
            q.AddWhere(columnName, oValue);
            CheckLogicalDelete(q);

            return DataService.GetReader(q.BuildSelectCommand());
        }

        /// <summary>
        /// Returns all records for the given column, comparison operator, and parameter
        /// This overload is used for queries that don't use and '=' operator, i.e. IS, IS NOT, etc.
        /// </summary>
        /// <param name="columnName">The name of the column, as defined in the database</param>
        /// <param name="comparison">The comparison operator used for the query</param>
        /// <param name="oValue">The value to match when fetching</param>
        /// <returns>IDataReader</returns>
        public static IDataReader FetchByParameter(string columnName, Comparison comparison, object oValue)
        {
            Query q = new Query(BaseSchema);
            q.AddWhere(columnName, comparison, oValue);
            CheckLogicalDelete(q);

            return DataService.GetReader(q.BuildSelectCommand());
        }

        /// <summary>
        /// Returns all records for the given column, comparison operator, and parameter
        /// in the order specified with the OrderBy parameter.
        /// This overload is used for queries that don't use and '=' operator, i.e. IS, IS NOT, etc,
        /// </summary>
        /// <param name="columnName">The name of the column, as defined in the database</param>
        /// <param name="comparison">The comparison operator used for the query</param>
        /// <param name="oValue">The value to match when fetching</param>
        /// <param name="orderBy">An OrderBy object used determine the order of results</param>
        /// <returns>IDataReader</returns>
        public static IDataReader FetchByParameter(string columnName, Comparison comparison, object oValue, OrderBy orderBy)
        {
            Query q = new Query(BaseSchema);
            q.AddWhere(columnName, comparison, oValue);
            q.OrderBy = orderBy;
            CheckLogicalDelete(q);

            return DataService.GetReader(q.BuildSelectCommand());
        }

        /// <summary>
        /// Returns all records for the given Query object
        /// </summary>
        /// <param name="query">Query for complex records</param>
        /// <returns>Generic Typed List</returns>
        public static IDataReader FetchByQuery(Query query)
        {
            CheckLogicalDelete(query);
            return DataService.GetReader(query.BuildSelectCommand());
        }

        /// <summary>
        /// Uses the passed-in object as a parameter set. Does not use the created/modified fields
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public static IDataReader Find(T item)
        {
            return Find(item, null);
        }

        /// <summary>
        /// Finds the specified item by building a SELECT query with conditions matching
        /// the values of the properties on the AbstractRecord object to the columns
        /// on the corresponding database table 
        /// </summary>
        /// <param name="item">The AbstractRecord derived type</param>
        /// <param name="orderBy">The OrberBy object used to return results</param>
        /// <returns></returns>
        public static IDataReader Find(T item, OrderBy orderBy)
        {
            //build the sql string and command statements at the same time
            Query q = new Query(BaseSchema);
            CheckLogicalDelete(q);

            //retrieve data from database

            foreach(TableSchema.TableColumn col in BaseSchema.Columns)
            {
                string columnName;
                object columnValue;
                columnName = col.ColumnName;
                columnValue = item.GetColumnValue<object>(columnName);
                if(!Utility.IsAuditField(columnName))
                {
                    object defaultValue = String.Empty;
                    switch(col.DataType)
                    {
                        case DbType.Boolean:
                            defaultValue = false;
                            break;
                        case DbType.Currency:
                        case DbType.Decimal:
                        case DbType.Int16:
                        case DbType.Double:
                        case DbType.Int32:
                            defaultValue = 0;
                            break;
                        case DbType.Date:
                        case DbType.DateTime:
                            defaultValue = new DateTime(1900, 1, 1);
                            break;
                        case DbType.Guid:
                            defaultValue = Guid.Empty;
                            break;
                    }
                    if(columnValue != null)
                    {
                        if(!columnValue.Equals(defaultValue))
                            q.AddWhere(columnName, columnValue);
                    }
                }
            }

            if(orderBy != null)
                q.OrderBy = orderBy;

            return DataService.GetReader(q.BuildSelectCommand());
        }

        #endregion

        #region "Persistence and Validation"

        protected internal virtual void BeforeLoad() { }
        protected internal virtual void AfterLoad() { }

        /// <summary>
        /// Executes before any operations occur within ActiveRecord Save() 
        /// </summary>
        protected internal virtual void BeforeValidate(CancelEventArgs e) { }

        /// <summary>
        /// Executes after a successful record validation 
        /// </summary>
        protected internal virtual void OnValidate() { }

        /// <summary>
        /// Executes after a successful record validation 
        /// </summary>
        protected internal virtual void AfterValidate() { }

        /// <summary>
        /// Executes on existing records after validation and before the update command has been generated.
        /// </summary>
        protected internal virtual void BeforeUpdate(CancelEventArgs e) { }

        /// <summary>
        /// Executes on existing records after validation and before the insert command has been generated.
        /// </summary>
        protected internal virtual void BeforeInsert(CancelEventArgs e) { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void AfterUpdate() { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void AfterInsert() { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void AfterCommit() { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void BeforeDelete(CancelEventArgs e) { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void AfterDelete() { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void BeforeDestroy(CancelEventArgs e) { }

        /// <summary>
        /// Executes after all steps have been performed for a successful ActiveRecord Save()
        /// </summary>
        protected internal virtual void AfterDestroy() { }

        /// <summary>
        /// Validates this instance.
        /// </summary>
        /// <returns>bool</returns>
        public bool Validate()
        {
            CancelEventArgs e = new CancelEventArgs(false);

            Errors.Clear();
            ValidationErrors.Clear();
            
            BeforeValidate(e);
            if (e.Cancel)
                return false;
            
            ValidateColumnSettings();

            OnValidate();

            bool success = (Errors.Count == 0) && (ValidationErrors.Count == 0);
            if (success)
            {
                AfterValidate();
            }
            return success;
        }
        
        #endregion


        /// <summary>
        /// Gets the select command used to retrieve the AbstractRecord object.
        /// </summary>
        /// <returns></returns>
        public QueryCommand GetSelectCommand()
        {
            return ActiveHelper<T>.GetSelectCommand(this);
        }

        /// <summary>
        /// Loads a the AbstractRecord by specifying an arbitrary column name and value
        /// as the retrieval criteria
        /// </summary>
        /// <param name="columnName">Name of the column to use as the retrieval key</param>
        /// <param name="paramValue">Value of the column to use as the retrieval criteria</param>
        public void LoadByParam(string columnName, object paramValue)
        {
            MarkOld();
            IDataReader rdr = null;
            try
            {
                rdr = new Query(BaseSchema).AddWhere(columnName, paramValue).ExecuteReader();
                if(rdr.Read())
                    Load(rdr);
                else
                    MarkNew(); //if no records, this is new still       
            }
            finally
            {
                if(rdr != null)
                    rdr.Close();
            }
        }

        /// <summary>
        /// Loads the record by fetching the underlying record with the passed value as the primary key
        /// </summary>
        /// <param name="keyID">The primary key value to retrieve the record for</param>
        public void LoadByKey(object keyID)
        {
            MarkOld();
            IDataReader rdr = null;
            try
            {
                Query q = new Query(BaseSchema).AddWhere(BaseSchema.PrimaryKey.ColumnName, keyID);
                //CheckLogicalDelete(q);
                rdr = q.ExecuteReader();
                if(rdr.Read())
                    Load(rdr);
                else
                    MarkNew(); //if no records, this is new still
            }
            finally
            {
                if(rdr != null)
                    rdr.Close();
            }
        }

        /// <summary>
        /// Return a new Query object based on the underlying TableSchema.Table type of the record
        /// </summary>
        /// <returns></returns>
        public static Query Query()
        {
            new T();
            return new Query(table);
        }

        /// <summary>
        /// Initializes the object using the default values for the underlying column data types.
        /// </summary>
        protected virtual void SetDefaults()
        {
            //initialize to default settings

            bool connectionClosed = true;
            bool setDbDefaults = DataService.GetInstance(ProviderName).SetPropertyDefaultsFromDatabase;

            if(DataService.GetInstance(ProviderName).CurrentSharedConnection != null)
                connectionClosed = DataService.GetInstance(ProviderName).CurrentSharedConnection.State == ConnectionState.Closed;

            foreach(TableSchema.TableColumn col in BaseSchema.Columns)
            {
                if(setDbDefaults && !String.IsNullOrEmpty(col.DefaultSetting) && connectionClosed)
                {
                    if(!Utility.IsMatch(col.DefaultSetting, SqlSchemaVariable.DEFAULT))
                    {
                        QueryCommand cmdDefault = new QueryCommand(SqlFragment.SELECT + col.DefaultSetting, col.Table.Provider.Name);
                        SetDefaultColumnValue(col.ColumnName, DataService.ExecuteScalar(cmdDefault));
                    }
                }
                else
                    SetDefaultColumnValue(col.ColumnName, Utility.GetDefaultSetting(col));
            }
            Initialize();
        }

        /// <summary>
        /// Forces object properties to be initialized using the defaults specified in the database schema.
        /// This method is called only if the provider level setting "useDatabaseDefaults" is set to <c>true</c>
        /// </summary>
        protected void ForceDefaults()
        {
            foreach(TableSchema.TableColumn col in BaseSchema.Columns)
            {
                if(!String.IsNullOrEmpty(col.DefaultSetting))
                {
                    if(!Utility.IsMatch(col.DefaultSetting, SqlSchemaVariable.DEFAULT))
                    {
                        QueryCommand cmdDefault = new QueryCommand(SqlFragment.SELECT + col.DefaultSetting, col.Table.Provider.Name);
                        SetDefaultColumnValue(col.ColumnName, DataService.ExecuteScalar(cmdDefault));
                    }
                }
                else
                    SetDefaultColumnValue(col.ColumnName, Utility.GetDefaultSetting(col));
            }
        }


        #region Serializers

        /// <summary>
        /// Returns and XML representation of the given record
        /// </summary>
        /// <returns></returns>
        public string ToXML()
        {
            Type type = typeof(T);
            XmlSerializer ser = new XmlSerializer(type);
            using(System.IO.MemoryStream stm = new System.IO.MemoryStream())
            {
                //serialize to a memory stream
                ser.Serialize(stm, this);

                //reset to beginning so we can read it.  
                stm.Position = 0;
                //Convert a string. 
                using(System.IO.StreamReader stmReader = new System.IO.StreamReader(stm))
                {
                    string xmlData = stmReader.ReadToEnd();
                    return xmlData;
                }
            }
        }

        /// <summary>
        /// Returns an object based on the passed-in XML.
        /// </summary>
        /// <param name="xml"></param>
        /// <returns></returns>
        public object NewFromXML(string xml)
        {
            object oOut = null;
            Type type = typeof(T);
            //hydrate based on private string var
            if(xml.Length > 0)
            {
                XmlSerializer serializer = new XmlSerializer(type);
                StringBuilder sb = new StringBuilder();
                sb.Append(xml);
                System.IO.StringReader sReader = new System.IO.StringReader(xml);
                oOut = serializer.Deserialize(sReader);
                //sb = null;
                sReader.Close();
            }

            return oOut;
        }

        #endregion


        #region Loaders

        /// <summary>
        /// Sets initial record states when a record is loaded.
        /// </summary>
        protected void SetLoadState()
        {
            IsLoaded = true;
            IsNew = false;
            Loaded();
        }

        /// <summary>
        /// Loads the object with the current reader's values. Assumes the reader is already moved to
        /// first position in recordset (aka has been "Read()")
        /// </summary>
        /// <param name="rdr"></param>
        public virtual void Load(IDataReader rdr)
        {
            BeforeLoad();

            // jeff_huntsman: we need to turn IsLoaded off while we load
            // for the OriginalValues to be setup again
            IsLoaded = false;   
            foreach(TableSchema.TableColumn col in BaseSchema.Columns)
            {
                try
                {
                    SetColumnValue(col.ColumnName, rdr[col.ColumnName]);
                    SetOriginalColumnValue(col.ColumnName, rdr[col.ColumnName]);
                }
                catch(Exception x)
                {
                    //turning off the Exception for now
                    //to support partial loads

                    //throw new Exception("Unable to set column value for " + col.ColumnName + ": " + x.Message);
                }
            }
            
            SetLoadState();
            MarkClean();

            AfterLoad();
        }

        /// <summary>
        /// Loads the object with the current reader's values. Assumes the reader is already moved to 
        /// first position in recordset (aka has been "Read()")
        /// </summary>
        /// <param name="tbl"></param>
        public virtual void Load(DataTable tbl)
        {
            if(tbl.Rows.Count > 0)
            {
                DataRow dr = tbl.Rows[0];
                Load(dr);
            }
        }

        /// <summary>
        /// Loads the object with the current DataRow's values. 
        /// </summary>
        /// <param name="dr"></param>
        public virtual void Load(DataRow dr)
        {
            BeforeLoad();

            // jeff_huntsman: we need to turn IsLoaded off while we load
            // for the OriginalValues to be setup again
            IsLoaded = false;   
            foreach(TableSchema.TableColumn col in BaseSchema.Columns)
            {
                try
                {
                    SetColumnValue(col.ColumnName, dr[col.ColumnName]);
                    SetOriginalColumnValue(col.ColumnName, dr[col.ColumnName]); 
                }
                catch(Exception x)
                {
                    //turning off the Exception for now
                    //TODO: move this to a SubSonicLoadException
                    //which collects the exceptions

                    //this will happen only if there's a reader error.
                    //throw new Exception("Unable to set column value for " + col.ColumnName + "; " + x.Message);
                }
            }
            SetLoadState();
            MarkClean();

            AfterLoad();
        }

        /// <summary>
        /// Opens the IDataReader, loads the object and closes the IDataReader. Unlike AbstractList.LoadAndCloseReader,
        /// this method does not assume that reader is open and in the first position!
        /// </summary>
        /// <param name="rdr"></param>
        public void LoadAndCloseReader(IDataReader rdr)
        {
            if(rdr.Read())
                Load(rdr);
            if(!rdr.IsClosed)
                rdr.Close();
        }

        #endregion


        #region Utility

        /// <summary>
        /// Returns the current collection of column settings for the given record.
        /// </summary>
        /// <returns></returns>
        public TableSchema.TableColumnSettingCollection GetColumnSettings()
        {
            GetSchema();
            return columnSettings;
        }

        /// <summary>
        /// Copies the current instance to a new instance
        /// </summary>
        /// <returns>New instance of current object</returns>
        public T Clone()
        {
            //copy this instance to a new instance
            T thisInstance = new T();

            foreach(TableSchema.TableColumnSetting setting in columnSettings)
                thisInstance.SetColumnValue(setting.ColumnName, setting.CurrentValue);
            return thisInstance;
        }

        /// <summary>
        /// Copies current instance to the passed in instance
        /// </summary>
        /// <param name="copyInstance"></param>
        public void CopyTo(T copyInstance)
        {
            if(copyInstance == null)
                copyInstance = new T();

            foreach(TableSchema.TableColumnSetting setting in columnSettings)
                copyInstance.SetColumnValue(setting.ColumnName, setting.CurrentValue);
        }

        /// <summary>
        /// Adds a new row to a Datatable with this record.
        /// Column names must match for this to work properly.
        /// </summary>
        /// <param name="dataTable"></param>
        public void CopyTo(DataTable dataTable)
        {
            DataRow newRow = dataTable.NewRow();
            foreach(TableSchema.TableColumnSetting setting in columnSettings)
            {
                try
                {
                    newRow[setting.ColumnName] = setting.CurrentValue;
                }
                catch {} //swallow this - this is a forgiving procedure :)
            }
            dataTable.Rows.Add(newRow);
        }

        /// <summary>
        /// Copies a record from a DataTable to this instance. Column names must match.
        /// </summary>
        /// <param name="row"></param>
        public void CopyFrom(DataRow row)
        {
            foreach(TableSchema.TableColumnSetting setting in columnSettings)
            {
                try
                {
                    setting.CurrentValue = row[setting.ColumnName];                    
                }
                catch {} //swallow this - this is a forgiving procedure :)
            }
        }

        /// <summary>
        /// Copies the passed-in instance settings to this instance
        /// </summary>
        /// <param name="copyInstance"></param>
        public void CopyFrom(T copyInstance)
        {
            if(copyInstance != null)
            {
                foreach(TableSchema.TableColumnSetting setting in copyInstance.columnSettings)
                    SetColumnValue(setting.ColumnName, setting.CurrentValue);                    
            }
            else
                throw new Exception("Copy instance is null");
        }

        #endregion


        #region WebUI Helper

        private readonly ValidationErrorCollection validationErrors = new ValidationErrorCollection();
        private readonly List<string> errorList = new List<string>();
        private string invalidTypeExceptionMessage = "{0} is not a valid {1}";
        private string lengthExceptionMessage = "{0} exceeds the maximum length of {1}";
        private string nullExceptionMessage = "{0} requires a value";

        /// <summary>
        /// Gets or sets the exception message thrown for non-null conversion errors performed during record validation.
        /// </summary>
        /// <value>The null exception message.</value>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string NullExceptionMessage
        {
            get { return nullExceptionMessage; }
            protected set { nullExceptionMessage = value; }
        }

        /// <summary>
        /// Gets or sets the exception message thrown for type conversion errors performed during record validation.
        /// </summary>
        /// <value>The invalid type exception message.</value>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string InvalidTypeExceptionMessage
        {
            get { return invalidTypeExceptionMessage; }
            protected set { invalidTypeExceptionMessage = value; }
        }

        /// <summary>
        /// Gets or sets the exception message thrown for value length conversion errors performed during record validation.
        /// </summary>
        /// <value>The length exception message.</value>
        [XmlIgnore]
        [HiddenForDataBinding(true)]
        public string LengthExceptionMessage
        {
            get { return lengthExceptionMessage; }
            protected set { lengthExceptionMessage = value; }
        }

        /// <summary>
        /// Gets the collection of error messages for the record.
        /// </summary>
        /// <value></value>
        [HiddenForDataBinding(true)]
        protected internal List<string> Errors
        {
            get { return errorList; }
        }
                
        public ValidationErrorCollection ValidationErrors
        {
            get { return this.validationErrors; }
        }

        /// <summary>
        /// Determines whether this instance has errors.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if this instance has errors; otherwise, <c>false</c>.
        /// </returns>
        public bool HasErrors()
        {
            return errorList.Count > 0;
        }

        /// <summary>
        /// Returns the list of error messages for the record.
        /// </summary>
        /// <returns></returns>
        public List<string> GetErrors()
        {
            return errorList;
        }

        /// <summary>
        /// Loops the underlying settings collection to validate type, nullability, and length
        /// </summary>
        public void ValidateColumnSettings()
        {
            //loop the current settings
            //make sure they are valid for their type
            foreach (TableSchema.TableColumnSetting setting in GetColumnSettings())
            {
                Utility.WriteTrace("Validating " + setting.ColumnName);
                object settingValue = setting.CurrentValue;
                bool isNullValue = (settingValue == null || settingValue == DBNull.Value);
                TableSchema.TableColumn col = table.GetColumn(setting.ColumnName);
                                
                if(!col.IsReadOnly)
                {
                    string formattedName = Utility.ParseCamelToProper(col.ColumnName);
                    Type t = col.GetPropertyType();
                                                    
                    //Convert the existing value to the type for this column
                    //if there's an error, report it.
                    //OK to bypass if the column is nullable and this setting is null
                    //just check for now if the value isn't null - it will be checked
                    //later for nullability
                    if(!col.IsNullable && !isNullValue)
                    {
                        try
                        {
                            if(col.DataType != DbType.Guid)
                                Convert.ChangeType(settingValue, t);
                        }
                        catch
                        {
                            //there's a conversion problem here
                            //add it to the Exception List<>
                            if(col.IsNumeric)
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "number"));
                            else if(col.IsDateTime)
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "date"));
                            else
                                errorList.Add(String.Format(InvalidTypeExceptionMessage, formattedName, "value"));
                        }
                    }

                    bool isDbControlledAuditField = (Utility.IsAuditField(col.ColumnName) && !String.IsNullOrEmpty(col.DefaultSetting));

                    //now make sure that this column's null settings match with what's in the setting
                    Utility.WriteTrace("Testing nullability of " + setting.ColumnName);
                    if(!col.IsNullable && isNullValue && !isDbControlledAuditField)
                    {
                        Utility.WriteTrace("Null Error Caught " + setting.ColumnName);
                        errorList.Add(String.Format(NullExceptionMessage, formattedName));
                    }

                    //finally, check the length
                    Utility.WriteTrace("Testing Max Length of " + setting.ColumnName);
                    if(!isNullValue && col.MaxLength > 0)
                    {
                        if(col.DataType != DbType.Boolean && settingValue.ToString().Length > col.MaxLength)
                        {
                            Utility.WriteTrace("Max Length Exceeded " + col.ColumnName + " can't exceed " + col.MaxLength + "; current value is set to " +
                                               settingValue.ToString().Length);
                            errorList.Add(String.Format(LengthExceptionMessage, formattedName, col.MaxLength));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Loads your object from an ASP.NET form postback
        /// </summary>
        public void LoadFromPost()
        {
            LoadFromPost(true);
        }

        /// <summary>
        /// Loads your object from an ASP.NET form postback
        /// </summary>
        /// <param name="validatePost">Set this to false to skip validation</param>
        public void LoadFromPost(bool validatePost)
        {
            if(System.Web.HttpContext.Current != null)
            {
                //use Request.form, since the ControlCollection can return weird results based on 
                //the container structure.
                System.Collections.Specialized.NameValueCollection formPost = System.Web.HttpContext.Current.Request.Form;
                TableSchema.TableColumnSettingCollection settings = GetColumnSettings();

                if(formPost != null && settings != null)
                {
                    foreach(string s in formPost.AllKeys)
                    {
                        Utility.WriteTrace("Looking at form field " + s);

                        foreach(TableSchema.TableColumnSetting setting in settings)
                        {
                            if(s.ToLower().EndsWith("_" + setting.ColumnName.ToLower()) || s.ToLower().EndsWith("$" + setting.ColumnName.ToLower()) ||
                               s.ToLower().Equals(setting.ColumnName.ToLower()))
                            {
                                setting.CurrentValue = formPost[s];                                
                                Utility.WriteTrace("Matched " + s + " to " + setting.ColumnName);
                            }
                        }
                    }
                }
                //validate the settings, since we're setting the object values here, not
                //using the accessors as we should be.
                if(validatePost)
                {
                    ValidateColumnSettings();

                    if(errorList.Count > 0)
                    {
                        //format this for the web
                        if(System.Web.HttpContext.Current != null)
                        {
                            //decorate the output
                            string errorReport = "<b>Validation Error:<b><ul>";
                            foreach(string s in errorList)
                                errorReport += "<li><i>" + s + "</i></li>";
                            errorReport += "</ul>";
                            throw new Exception(errorReport);
                        }
                        else
                        {
                            throw new Exception(
                                "Validation error - catch this and check the ExceptionList property to review the exceptions. You can change the output message as needed by accessing the ExceptionMessage properties of this object");
                        }
                    }
                }
            }
        }

        #endregion


        #region Utility

        /// <summary>
        /// If this object has a logical delete column, this method will append in the required parameter to avoid returning 
        /// deleted records
        /// </summary>
        /// <param name="q"></param>
        internal static void CheckLogicalDelete(Query q)
        {
            q.CheckLogicalDelete();
        }

        /// <summary>
        /// Returns an ordered ListItemCollection for use with DropDowns, RadioButtonLists, and CheckBoxLists
        /// Note: This method assumes that the column in the second position is the text value column
        /// </summary>
        /// <returns></returns>
        public static ListItemCollection GetListItems()
        {
            //get the textColumn based on position
            //which should be the second column of the table
            string textColumn = BaseSchema.Columns[1].ColumnName;
            return GetListItems(textColumn);
        }

        /// <summary>
        /// Returns an ordered ListItemCollection for use with DropDowns, RadioButtonLists, and CheckBoxLists
        /// </summary>
        /// <param name="textColumn">The name of the column which should be used as the text value column</param>
        /// <returns></returns>
        public static ListItemCollection GetListItems(string textColumn)
        {
            //T item = new T();
            ListItemCollection list = new ListItemCollection();
            string pkCol = BaseSchema.PrimaryKey.ColumnName;
            string textCol = BaseSchema.GetColumn(textColumn).ColumnName;

            //run a query retrieving the two columns
            Query q = new Query(BaseSchema);
            q.SelectList = pkCol + "," + textCol;
            q.OrderBy = OrderBy.Asc(textCol);
            IDataReader rdr = q.ExecuteReader();

            while(rdr.Read())
            {
                ListItem listItem = new ListItem(rdr[1].ToString(), rdr[0].ToString());
                list.Add(listItem);
            }
            return list;
        }

        #endregion
        
        #region ISerializable Members

        public AbstractRecord(SerializationInfo info, StreamingContext context) : this()            
        {            
            this._isLoaded = info.GetBoolean("IsLoaded");
            this._isNew = info.GetBoolean("IsNew");
            this.errorList = info.GetValue("ErrorList", typeof(System.Collections.Generic.List<string>)) as System.Collections.Generic.List<string>;
        }

        public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("IsLoaded", this.IsLoaded);
            info.AddValue("IsNew", this.IsNew);
            info.AddValue("ErrorList", this.Errors);
        }

        #endregion
    }

    /// <summary>
    /// Attribute class used to suppress "internal" properties when databinding. YMMV.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    internal sealed class HiddenForDataBindingAttribute : Attribute
    {
        private readonly bool _isHidden;
        public HiddenForDataBindingAttribute() {}

        public HiddenForDataBindingAttribute(bool isHidden)
        {
            _isHidden = isHidden;
        }

        public bool IsHidden
        {
            get { return _isHidden; }
        }
    }
}
